﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using Red.Controls.Diagrams.Actions.InteropActions;
using Red.Controls.Diagrams.Declarators;
using Red.Controls.Diagrams.ViewModels;
using Red.Controls.ViewModels;
using Red.Core.Extensions;
using Red.EngineData.View;

namespace Red.Controls.Diagrams.InteropUtils
{
    class InteropObjectManager : BaseObjectManager, IObjectManager
    {
        #region Data

        private readonly DataView _rootEngineObject;
        private DataItem _rootDataItem;

        #endregion Data

        #region C-tor

        public InteropObjectManager( DataView rootEngineObject, DiagramViewModel viewModel, NodeDeclarators declarators )
            : base( declarators, viewModel )
        {
            _rootEngineObject = rootEngineObject;            

            _rootEngineObject.RequestNotifier( "_nodeCreated", notifier => notifier.OnNotifyEvent += NodeCreated_OnNotifyEvent );
            _rootEngineObject.RequestNotifier( "_nodeRemoved", notifier => notifier.OnNotifyEvent += NodeRemoved_OnNotifyEvent );
            _rootEngineObject.RequestNotifier( "_nodeMoved", notifier => notifier.OnNotifyEvent += NodeMoved_OnNotifyEvent );
            _rootEngineObject.RequestNotifier( "_connectionCreated", notifier => notifier.OnNotifyEvent += ConnectionCreated_OnNotifyEvent );
            _rootEngineObject.RequestNotifier( "_connectionRemoved", notifier => notifier.OnNotifyEvent += ConnectionRemoved_OnNotifyEvent );
        }

        #endregion C-tor

        #region IObjectManager implementation

        private async Task LoadAsync( string parentID )
        {
            if ( _rootDataItem == null )
            {
                var tcs = new TaskCompletionSource<bool>();

                InteropDataItem.CreateRoot( _rootEngineObject,
                async dataItem =>
                {
                    _rootDataItem = dataItem;
                    await DownloadAndInitializeDiagram( parentID );
                    tcs.SetResult( true );
                } );

                await tcs.Task;
            }
            else
            {
                await DownloadAndInitializeDiagram( parentID );
            }
        }

        public void CreateObject( string className, Point position )
        {
            if ( _rootEngineObject == null )
                return;

            var createObjectAction = new AddNodeAction( _rootDataItem, className, position, _viewModel, CreateNode );
            createObjectAction.Execute().DoNotAwait();
        }

        public void CreateConnectedObject( string className, Point position, ConnectionViewModel connection )
        {
            var action = new AddConnectedNodeAction( _rootDataItem, className, position, _viewModel, connection, CreateNode );
            action.ExecuteAsync().DoNotAwait();
        }

        public void DeleteSelectedNodes()
        {
            // Take a copy of the list of all nodes so we can delete selected nodes while iterating.
            var nodesCopy = _viewModel.Network.Nodes.ToArray();
            var nodes = nodesCopy.Where( item => item.IsSelected && item.Object.IsRoot == false );

            if ( nodes.Any() )
            {
                var action = new RemoveNodesAction( _rootDataItem, nodes, _viewModel );
                action.Execute();
            }
        }

        public void MoveNodes( IEnumerable<Tuple<double, double, string, NodeViewModel>> nodesData )
        {
            if ( nodesData.Any() )
            {
                var moveNodeAction = new MoveNodesAction( _rootDataItem, _viewModel, nodesData );
                moveNodeAction.Execute();
            }
        }

        public void CutSelectedNodes()
        {
            // TODO
        }

        public bool CanCopySelectedNodes()
        {
            // TODO
            return false;
        }

        public void CopySelectedNodes()
        {
            // TODO
        }

        public bool CanPasteNodes()
        {
            // TODO
            return false;
        }

        public void PasteNodes( Point position )
        {
            // TODO
        }

        public void CreateConnection( ConnectorViewModel connectorDraggedOut, ConnectorViewModel connectorDraggedOver, ConnectionViewModel newConnection )
        {
            var action = new AddConnectionAction( _viewModel, connectorDraggedOut, connectorDraggedOver, newConnection, _rootDataItem );
            action.Execute();
        }

        public void DeleteConnection( ConnectionViewModel connection )
        {
            var connections = new List<ConnectionViewModel>();
            connections.Add( connection );

            DeleteConnections( connections );
        }

        public void DeleteConnections( IEnumerable<ConnectionViewModel> connections )
        {
            var action = new RemoveConnectionsAction( _viewModel, _rootDataItem, connections );
            action.Execute();
        }

        public async Task CreateDiagramAsync( string parentID )
        {
            await LoadAsync( parentID );
        }

        public bool ObjectHasOutputOfType( string nodeType, string outputType )
        {
            return false;
        }

        public async Task<bool> ObjectHasOutputOfTypeAsync( string nodeType, string outputType )
        {
            var declarator = _declarators.GetNodeDeclarator( nodeType );

            foreach ( var output in declarator.Outputs )
            {
                // we need to add "handle:" to type name because all binding properties are handles to objects.
                var result = await _rootDataItem.ExecuteAsync<bool>( "canCreateConnection", nodeType, output.Binding, $"handle:{outputType}" );
                if ( result.Value )
                    return true;
            }
            return false;
        }

        public void CreateDecoratorObject( string className, Point position, ConnectionViewModel connection )
        {
            var action = new AddDecoratorNodeAction( _rootDataItem, className, position, connection, _viewModel, CreateNode );
            action.ExecuteAsync().DoNotAwait();
        }

        #endregion IObjectManager implementation

        #region Private methods

        private async Task DownloadAndInitializeDiagram( string parentID )
        {
            await _rootDataItem.ExecuteAsync( "loadGraph", parentID );

            // download empty interop objects
            var interopObjects = ( await DownloadDiagram() ).ToList();

            // initialize interop objects with their values
            await InitializeDiagram( interopObjects );

            // create nodes
            var nodeViewModels = interopObjects.Select( CreateNodeViewModel ).ToList();

            // create connections
            var connectionViewModels = CreateConnections( nodeViewModels ).ToList();

            // create diagrams
            _viewModel.CreateDiagram( nodeViewModels, connectionViewModels );
        }

        private async Task<IEnumerable<InteropObject>> DownloadDiagram()
        {
            var nodesDataItem = await _rootDataItem.DigUpSubItemAsync( "nodes" ) as InteropDataItem;
            await nodesDataItem.RefreshAsync();
            var tasks = nodesDataItem.Data.Children.Select( async item =>
            {
                var nodeDataItem = await nodesDataItem.DigUpSubItemAsync( item );
                return new InteropObject( nodeDataItem );
            } );

            return await Task.WhenAll( tasks );
        }

        private async Task InitializeDiagram( IEnumerable<InteropObject> objects )
        {
            var tasks = objects.Select( async interopObject => 
            {
                var initializer = new InteropObjectInitializer( _declarators );
                await interopObject.Accept( initializer );
            } );

            await Task.WhenAll( tasks );
        }

        private void NodeRemoved_OnNotifyEvent( string nodeID )
        {
            var node = _viewModel.FindNode( nodeID );
            if ( node != null )
                _viewModel.DeleteNode( node );
        }

        private async void NodeMoved_OnNotifyEvent( string nodeID )
        {
            var interopNodeDownloader = new InteropNodeDownloader( _rootDataItem, nodeID, _declarators );
            var interopObject = await interopNodeDownloader.Execute();
            var node = _viewModel.FindNode( interopObject.ID );
            node.Object = interopObject;
        }

        private async void NodeCreated_OnNotifyEvent( string nodeID )
        {
            await CreateNode( nodeID );
        }

        private async Task CreateNode( string nodeID )
        {
            var interopNodeDownloader = new InteropNodeDownloader( _rootDataItem, nodeID, _declarators );
            var interopObject = await interopNodeDownloader.Execute();

            //Creating NodeViewModel
            var nodeViewModel = _viewModel.FindNode( nodeID );
            if ( nodeViewModel == null )
            {
                nodeViewModel = InteropNodeViewModelCreator( interopObject );
                _viewModel.AddNode( nodeViewModel );
            }

            RecreateNodeConnections( nodeViewModel, interopObject );
        }

        private async void ConnectionCreated_OnNotifyEvent( string nodeID )
        {
            var interopNodeDownloader = new InteropNodeDownloader( _rootDataItem, nodeID, _declarators );
            var interopObject = await interopNodeDownloader.Execute();
            OnConnectionFinished( interopObject );
        }

        private async void ConnectionRemoved_OnNotifyEvent( string nodeID )
        {
            var interopNodeDownloader = new InteropNodeDownloader( _rootDataItem, nodeID, _declarators );
            var interopObject = await interopNodeDownloader.Execute();
            OnConnectionFinished( interopObject );
        }

        private void OnConnectionFinished( InteropObject interopObject )
        {
            var nodeViewModel = _viewModel.FindNode( interopObject.ID );

            // nodeViewModel will be null if we removed multiple nodes that might be connected in one action
            // On the engine side that nodes already does not exist.
            if ( nodeViewModel == null )
                return;

            // refresh underlying IObject
            nodeViewModel.Object = interopObject;

            RecreateNodeConnections( nodeViewModel, interopObject );
        }

        private void RecreateNodeConnections( NodeViewModel nodeViewModel, InteropObject interopObject )
        {
            // refresh underlying IProperty objects on connectors
            foreach ( var outputConnector in nodeViewModel.OutputConnectors )
            {
                var propertyName = outputConnector.Property.Name;
                var interopProperty = interopObject.GetProperty( propertyName );
                outputConnector.Property = interopProperty;
            }

            // remove all connections going out of output connectors (they represent binding properties from node declarators)
            var attachedConnections = nodeViewModel.OutputConnectors.SelectMany( connector => connector.AttachedConnections );
            attachedConnections.ToList().ForEach( connection => _viewModel.DeleteConnection( connection ) );

            // recreate connections
            var nodeViewModels = _viewModel.Network.Nodes.ToList();
            var newConnections = CreateConnections( nodeViewModels );
            newConnections.ToList().ForEach( connection => _viewModel.AddConnection( connection ) );

			// quick fix
            _viewModel.DeleteInvalidConnections();
        }

        private NodeViewModel InteropNodeViewModelCreator( InteropObject interopObject )
        {
            return CreateNodeViewModel( interopObject );
        }

        #endregion Private methods
    }
}